From 32c93ea4366936a716e288fe4bb986659be54bed Mon Sep 17 00:00:00 2001 From: =?utf8?q?IOhannes=20m=20zm=C3=B6lnig=20=28Debian/GNU=29?= Date: Wed, 30 Apr 2025 09:39:57 +0200 Subject: [PATCH] New upstream version 2.6.0+ds --- CMakeLists.txt | 3 +- docs/Build/Linux.md | 2 + docs/changelog.yml | 20 +++ .../oscpp/include/oscpp/detail/endian.hpp | 3 +- linux/Dockerfile.build | 14 +- ..._11.ini => meson_native_minversion_12.ini} | 2 +- ...l_11.ini => meson_native_universal_12.ini} | 2 +- meson.build | 52 ++++--- src/AudioInterface.cpp | 24 ++-- src/AudioInterface.h | 4 + src/AudioSocket.cpp | 2 +- src/JackTrip.h | 9 ++ src/JackTripWorker.h | 6 + src/OscServer.cpp | 54 ++++++-- src/OscServer.h | 9 +- src/Regulator.cpp | 19 +-- src/RtAudioInterface.cpp | 12 +- src/UdpHubListener.cpp | 22 +++ src/UdpHubListener.h | 6 + src/gui/qjacktrip.cpp | 36 +++-- src/jacktrip_globals.h | 2 +- src/vs/AudioSettings.qml | 1 - src/vs/ChangeDevices.qml | 92 ++++++++++--- src/vs/Connected.qml | 9 +- src/vs/CreateStudio.qml | 5 +- src/vs/DeviceWarningModal.qml | 1 - src/vs/FeedbackSurvey.qml | 2 +- src/vs/Footer.qml | 9 +- src/vs/Setup.qml | 1 - src/vs/WebEngine.qml | 3 +- src/vs/WebView.qml | 3 +- src/vs/virtualstudio.cpp | 130 ++++++++++++------ src/vs/vsAudio.cpp | 106 +++++++------- src/vs/vsDevice.cpp | 22 ++- 34 files changed, 478 insertions(+), 209 deletions(-) rename macos/{meson_native_minversion_11.ini => meson_native_minversion_12.ini} (82%) rename macos/{meson_native_universal_11.ini => meson_native_universal_12.ini} (88%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 442eaaf..cb9eab7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,7 +101,7 @@ endif () string(PREPEND QtVersion "Qt") -if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin") +if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin" OR ${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") find_package(PkgConfig REQUIRED) pkg_check_modules(JACK REQUIRED IMPORTED_TARGET jack) if (weakjack) @@ -218,6 +218,7 @@ if (NOT nogui) src/vs/vsWebSocket.cpp src/vs/vsPermissions.cpp src/vs/vs.qrc + src/vs/WebSocketTransport.cpp src/images/images.qrc src/Analyzer.cpp src/Monitor.cpp diff --git a/docs/Build/Linux.md b/docs/Build/Linux.md index 8086733..9f26216 100644 --- a/docs/Build/Linux.md +++ b/docs/Build/Linux.md @@ -134,6 +134,7 @@ available: * MESON_ARGS - arguments to build using meson * QT_DOWNLOAD_URL - path to qt6 download (optional) * VST3SDK_DOWNLOAD_URL - path to the VST3 SDK (optional) +* USE_SYSTEM_LIBSAMPLERATE - dynamically link with libsamplerate For example: @@ -171,6 +172,7 @@ arm32 static ``` docker buildx build --target=artifact -f linux/Dockerfile.build --output type=local,dest=./ \ --platform linux/arm/v7 --build-arg BUILD_CONTAINER=debian:buster \ + --build-arg USE_SYSTEM_LIBSAMPLERATE=1 \ --build-arg MESON_ARGS="-Ddefault_library=static -Drtaudio=enabled -Drtaudio:jack=disabled -Drtaudio:default_library=static -Drtaudio:alsa=enabled -Drtaudio:pulse=disabled -Drtaudio:werror=false -Dnogui=true -Dcpp_link_args='-no-pie'" \ --build-arg QT_DOWNLOAD_URL=https://files.jacktrip.org/contrib/qt/qt-5.15.13-static-linux-arm32.tar.gz . ``` diff --git a/docs/changelog.yml b/docs/changelog.yml index 6784c61..82fd0a5 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -1,3 +1,23 @@ +- Version: "2.6.0" + Date: 2025-04-22 + Description: + - (added) OSC endpoint to get latencies for connected clients + - (updated) PLC auto headroom allows higher latency when necessary + - (updated) VS Mode allow any two consecutive channels for input + - (updated) VS Mode easier audio switching between stereo and mono + - (updated) VS Mode latency statistics now include jitter buffer + - (updated) VS Mode improvements to audio quality override settings + - (updated) VS Mode temporarily disabling feedback detection + - (fixed) VS Mode kicked out of sessions due to studio change + - (fixed) VS Mode recognizes changes to server host and port + - (fixed) VS Mode bugs with reconnecting due to audio changes + - (fixed) VS Mode strange error message during startup on Linux + - (fixed) VS Mode empty studio list when starting up + - (fixed) Ability to build VS Mode using CMake + - (fixed) Ability to build aarch64 or armv7 on Alpine Linux + - (fixed) Ability to build using Qt 6.9 release candidates + - (fixed) Ability to disable the use of libsamplerate + - (fixed) Ignore timestamps when generating jacktrip.1.gz - Version: "2.5.1" Date: 2025-01-30 Description: diff --git a/externals/oscpp/include/oscpp/detail/endian.hpp b/externals/oscpp/include/oscpp/detail/endian.hpp index f9a0c2e..bdb061c 100644 --- a/externals/oscpp/include/oscpp/detail/endian.hpp +++ b/externals/oscpp/include/oscpp/detail/endian.hpp @@ -71,7 +71,8 @@ defined(__ia64__) || defined(_M_IX86) || defined(_M_IA64) || \ defined(_M_ALPHA) || defined(__amd64) || defined(__amd64__) || \ defined(_M_AMD64) || defined(__x86_64) || defined(__x86_64__) || \ - defined(_M_X64) || defined(__bfin__) + defined(_M_X64) || defined(__bfin__) || defined(__aarch64__) || \ + defined(__ARM_EABI__) # define OSCPP_LITTLE_ENDIAN # define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_LITTLE_ENDIAN diff --git a/linux/Dockerfile.build b/linux/Dockerfile.build index 4af7d42..e0bdbae 100644 --- a/linux/Dockerfile.build +++ b/linux/Dockerfile.build @@ -3,9 +3,10 @@ # this Dockerfile is used by GitHub CI to create linux builds # it requires these environment variables: # -# BUILD_CONTAINER - Debian based container image to build with -# MESON_ARGS - arguments to build using meson -# QT_DOWNLOAD_URL - path to qt download (optional) +# BUILD_CONTAINER - Debian based container image to build with +# MESON_ARGS - arguments to build using meson +# QT_DOWNLOAD_URL - path to qt download (optional) +# USE_SYSTEM_LIBSAMPLERATE - dynamically link with libsamplerate # container image versions ARG BUILD_CONTAINER=ubuntu:20.04 @@ -25,6 +26,13 @@ RUN python3 -m pip install --upgrade pip \ WORKDIR /opt/jacktrip +# install libsamplerate +ARG USE_SYSTEM_LIBSAMPLERATE="" +ENV USE_SYSTEM_LIBSAMPLERATE=$USE_SYSTEM_LIBSAMPLERATE +RUN if [ -n "$USE_SYSTEM_LIBSAMPLERATE" ]; then \ + apt-get install -yq --no-install-recommends libsamplerate-dev; \ + fi + # install qt ARG QT_DOWNLOAD_URL="" ENV QT_DOWNLOAD_URL=$QT_DOWNLOAD_URL diff --git a/macos/meson_native_minversion_11.ini b/macos/meson_native_minversion_12.ini similarity index 82% rename from macos/meson_native_minversion_11.ini rename to macos/meson_native_minversion_12.ini index 6d85821..184e398 100644 --- a/macos/meson_native_minversion_11.ini +++ b/macos/meson_native_minversion_12.ini @@ -1,5 +1,5 @@ [constants] -minversion_args = ['-mmacosx-version-min=11'] +minversion_args = ['-mmacosx-version-min=12'] [built-in options] c_args = minversion_args diff --git a/macos/meson_native_universal_11.ini b/macos/meson_native_universal_12.ini similarity index 88% rename from macos/meson_native_universal_11.ini rename to macos/meson_native_universal_12.ini index c91f052..973f4b2 100644 --- a/macos/meson_native_universal_11.ini +++ b/macos/meson_native_universal_12.ini @@ -1,5 +1,5 @@ [constants] -minversion_args = ['-mmacosx-version-min=11'] +minversion_args = ['-mmacosx-version-min=12'] universal_args = ['-arch', 'x86_64', '-arch', 'arm64'] [built-in options] diff --git a/meson.build b/meson.build index 2bba8cb..259fa85 100644 --- a/meson.build +++ b/meson.build @@ -281,7 +281,17 @@ if get_option('default_library') == 'static' qt_plugindir = run_command(qmake, '-query', 'QT_INSTALL_PLUGINS', check : true).stdout().strip() if qt_version == '6' # qt6 requires "Bundled*" modules for linking - static_deps += dependency('qt6', modules: ['DBus', 'BundledLibpng', 'BundledPcre2', 'BundledHarfbuzz', 'BundledZLIB'], include_type: 'system') + static_deps += compiler.find_library('Qt6BundledLibpng', required : true, dirs : [qt_libdir]) + static_deps += compiler.find_library('Qt6BundledPcre2', required : true, dirs : [qt_libdir]) + static_deps += compiler.find_library('Qt6BundledHarfbuzz', required : true, dirs : [qt_libdir]) + zlib_dep = compiler.find_library('Qt6BundledZLIB', required : false, dirs : [qt_libdir]) + if zlib_dep.found() + static_deps += zlib_dep + endif + dbus_dep = compiler.find_library('Qt6DBus', required : false, dirs : [qt_libdir]) + if dbus_dep.found() + static_deps += dbus_dep + endif else static_deps += compiler.find_library('qtpcre2', required : true, dirs : [qt_libdir]) endif @@ -317,6 +327,8 @@ if get_option('default_library') == 'static' static_link_args += ['-framework', 'Security'] static_link_args += ['-framework', 'GSS'] static_link_args += ['-framework', 'SystemConfiguration'] + static_link_args += ['-framework', 'UniformTypeIdentifiers'] + static_link_args += '-lresolv' static_deps += dependency('zlib', required : true) endif endif @@ -344,27 +356,31 @@ if rtaudio_dep.found() == false and jack_dep.found() == false configure.''') endif -libsamplerate_dep = [] found_libsamplerate = false if get_option('libsamplerate').allowed() - opt_var = cmake.subproject_options() - if get_option('buildtype') == 'release' - opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Release'}) + libsamplerate_dep = dependency('samplerate', required: false) + if libsamplerate_dep.found() + found_libsamplerate = true else - opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Debug'}) - endif - opt_var.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'}) - libsamplerate_subproject = cmake.subproject('libsamplerate', options: opt_var) - libsamplerate_dep = libsamplerate_subproject.dependency('samplerate') - found_libsamplerate = libsamplerate_dep.found() - if not found_libsamplerate and not get_option('libsamplerate').auto() - error('failed to configure libsamplerate') - endif - if found_libsamplerate - defines += '-DHAVE_LIBSAMPLERATE' - deps += libsamplerate_dep + opt_var = cmake.subproject_options() + if get_option('buildtype') == 'release' + opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Release'}) + else + opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Debug'}) + endif + opt_var.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'}) + libsamplerate_subproject = cmake.subproject('libsamplerate', options: opt_var) + libsamplerate_dep = libsamplerate_subproject.dependency('samplerate') + found_libsamplerate = libsamplerate_dep.found() + if not found_libsamplerate and not get_option('libsamplerate').auto() + error('failed to configure libsamplerate') + endif endif endif +if found_libsamplerate + defines += '-DHAVE_LIBSAMPLERATE' + deps += libsamplerate_dep +endif if host_machine.system() == 'darwin' src += ['src/NoNap.mm'] @@ -530,7 +546,7 @@ if (host_machine.system() == 'linux') custom_target('jacktrip.1.gz', input: manfile, output: 'jacktrip.1.gz', - command: [gzip, '-k', '-f', '@INPUT@'], + command: [gzip, '-k', '-f', '-n', '@INPUT@'], install: true, install_dir: get_option('mandir') / 'man1') endif diff --git a/src/AudioInterface.cpp b/src/AudioInterface.cpp index 7450912..044cdbb 100644 --- a/src/AudioInterface.cpp +++ b/src/AudioInterface.cpp @@ -45,6 +45,12 @@ #include "JackTrip.h" #include "ProcessPlugin.h" +#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) +#define STD_AS_CONST qAsConst +#else +#define STD_AS_CONST std::as_const +#endif + using std::cout; using std::endl; @@ -76,6 +82,8 @@ AudioInterface::AudioInterface(QVarLengthArray InputChans, , mMonitorStarted(false) , mJackTrip(jacktrip) , mInputMixMode(InputMixMode) + , mAudioInputLatency(0) + , mAudioOutputLatency(0) , mProcessingAudio(false) { } @@ -189,7 +197,7 @@ void AudioInterface::audioInputCallback(QVarLengthArray& in_buffer, } #ifndef WAIR - for (auto& s : qAsConst(mAudioSockets)) { + for (auto& s : STD_AS_CONST(mAudioSockets)) { s->getFromAudioSocketPlugin()->compute(n_frames, in_buffer.data(), in_buffer.data()); } @@ -204,7 +212,7 @@ void AudioInterface::audioInputCallback(QVarLengthArray& in_buffer, #endif // not WAIR // process incoming signal from audio interface using process plugins - for (auto& p : qAsConst(mProcessPluginsToNetwork)) { + for (auto& p : STD_AS_CONST(mProcessPluginsToNetwork)) { if (p->getInited()) { p->compute(n_frames, in_buffer.data(), in_buffer.data()); } @@ -266,7 +274,7 @@ void AudioInterface::audioOutputCallback(QVarLengthArray& out_buffer, /// with one. do it chaining outputs to inputs in the buffers. May need a tempo buffer #ifndef WAIR // NOT WAIR: - for (auto& p : qAsConst(mProcessPluginsFromNetwork)) { + for (auto& p : STD_AS_CONST(mProcessPluginsFromNetwork)) { if (p->getInited()) { p->compute(n_frames, out_buffer.data(), out_buffer.data()); } @@ -308,7 +316,7 @@ void AudioInterface::audioOutputCallback(QVarLengthArray& out_buffer, } } - for (auto& s : qAsConst(mAudioSockets)) { + for (auto& s : STD_AS_CONST(mAudioSockets)) { s->getToAudioSocketPlugin()->compute(n_frames, out_buffer.data(), out_buffer.data()); } @@ -740,22 +748,22 @@ void AudioInterface::initPlugins(bool verbose) << ") at sampling rate " << mSampleRate << "\n"; } - for (auto& plugin : qAsConst(mProcessPluginsFromNetwork)) { + for (auto& plugin : STD_AS_CONST(mProcessPluginsFromNetwork)) { plugin->setOutgoingToNetwork(false); plugin->updateNumChannels(nChansIn, nChansOut); plugin->init(mSampleRate, mBufferSizeInSamples); } - for (auto& plugin : qAsConst(mProcessPluginsToNetwork)) { + for (auto& plugin : STD_AS_CONST(mProcessPluginsToNetwork)) { plugin->setOutgoingToNetwork(true); plugin->updateNumChannels(nChansIn, nChansOut); plugin->init(mSampleRate, mBufferSizeInSamples); } - for (auto& plugin : qAsConst(mProcessPluginsToMonitor)) { + for (auto& plugin : STD_AS_CONST(mProcessPluginsToMonitor)) { plugin->setOutgoingToNetwork(false); plugin->updateNumChannels(nChansMon, nChansMon); plugin->init(mSampleRate, mBufferSizeInSamples); } - for (auto& s : qAsConst(mAudioSockets)) { + for (auto& s : STD_AS_CONST(mAudioSockets)) { auto* plugin = s->getFromAudioSocketPlugin().get(); plugin->setOutgoingToNetwork(true); plugin->updateNumChannels(nChansIn, nChansOut); diff --git a/src/AudioInterface.h b/src/AudioInterface.h index 3de02af..886ad59 100644 --- a/src/AudioInterface.h +++ b/src/AudioInterface.h @@ -315,6 +315,8 @@ class AudioInterface const std::string& getDevicesErrorMsg() const { return mErrorMsg; } const std::string& getDevicesWarningHelpUrl() const { return mWarningHelpUrl; } const std::string& getDevicesErrorHelpUrl() const { return mErrorHelpUrl; } + double getAudioInputLatency() const { return mAudioInputLatency; } + double getAudioOutputLatency() const { return mAudioOutputLatency; } bool highLatencyBufferSize() const { return getBufferSizeInSamples() > 256; } bool getHighLatencyFlag() const { return mHighLatencyFlag; } //------------------------------------------------------------------ @@ -370,6 +372,8 @@ class AudioInterface protected: JackTrip* mJackTrip; ///< JackTrip Mediator Class pointer inputMixModeT mInputMixMode; ///< Input mixing mode + double mAudioInputLatency; ///< Latency of the audio input + double mAudioOutputLatency; ///< Latency of the audio output void setDevicesWarningMsg(warningMessageT msg); void setDevicesErrorMsg(errorMessageT msg); diff --git a/src/AudioSocket.cpp b/src/AudioSocket.cpp index 298f74d..f1f6fb9 100644 --- a/src/AudioSocket.cpp +++ b/src/AudioSocket.cpp @@ -585,7 +585,7 @@ void AudioSocketWorker::receiveAudio() void AudioSocketWorker::scheduleReconnect() { if (mRetryConnection) { - qDebug() << "Attempting to reconnect audio socket"; + cout << "Attempting to reconnect audio socket" << endl; if (mTimerPtr.isNull()) { mTimerPtr.reset(new QTimer); QObject::connect(mTimerPtr.data(), &QTimer::timeout, this, diff --git a/src/JackTrip.h b/src/JackTrip.h index 00b9cce..5a1142f 100644 --- a/src/JackTrip.h +++ b/src/JackTrip.h @@ -551,6 +551,15 @@ class JackTrip : public QObject return (mAudioInterface == nullptr) ? false : mAudioInterface->getHighLatencyFlag(); } + double getAudioInputLatency() const + { + return (mAudioInterface == nullptr) ? 0 : mAudioInterface->getAudioInputLatency(); + } + double getAudioOutputLatency() const + { + return (mAudioInterface == nullptr) ? 0 + : mAudioInterface->getAudioOutputLatency(); + } double getLatency() const { return mReceiveRingBuffer == nullptr ? -1 : mReceiveRingBuffer->getLatency(); diff --git a/src/JackTripWorker.h b/src/JackTripWorker.h index 091f983..867586c 100644 --- a/src/JackTripWorker.h +++ b/src/JackTripWorker.h @@ -136,6 +136,12 @@ class JackTripWorker : public QObject uint16_t getClientPort() { return mClientPort; } QString getClientAddress() { return mClientAddress; } + double getLatency() + { + QMutexLocker lock(&mMutex); + return mJackTrip.isNull() ? -1 : mJackTrip->getLatency(); + } + private slots: void slotTest() { std::cout << "--- JackTripWorker TEST SLOT ---" << std::endl; } void receivedDataUDP(); diff --git a/src/OscServer.cpp b/src/OscServer.cpp index 5c3b61b..30c94b6 100644 --- a/src/OscServer.cpp +++ b/src/OscServer.cpp @@ -38,8 +38,7 @@ #include -using std::cout; -using std::endl; +using namespace std; //******************************************************************************* OscServer::OscServer(quint16 port, QObject* parent) : QObject(parent), mPort(port) {} @@ -91,10 +90,11 @@ void OscServer::readPendingDatagrams() mOscServerSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); - qDebug() << "Received datagram from" << sender << ":" << senderPort; - qDebug() << " - Data:" << datagram; + // qDebug() << "Received datagram from" << sender << ":" << senderPort; + // qDebug() << " - Data:" << datagram; #ifndef NO_OSCPP - handlePacket(OSCPP::Server::Packet(datagram.data(), datagram.size())); + handlePacket(OSCPP::Server::Packet(datagram.data(), datagram.size()), sender, + senderPort); #endif // NO_OSCPP // Send a reply back to the client // QByteArray replyData("Reply from server"); @@ -104,7 +104,8 @@ void OscServer::readPendingDatagrams() //******************************************************************************* #ifndef NO_OSCPP -void OscServer::handlePacket(const OSCPP::Server::Packet& packet) +void OscServer::handlePacket(const OSCPP::Server::Packet& packet, + const QHostAddress& sender, quint16 senderPort) { try { if (packet.isBundle()) { @@ -115,7 +116,7 @@ void OscServer::handlePacket(const OSCPP::Server::Packet& packet) // Iterate over all the packets and call handlePacket recursively. while (!packets.atEnd()) { - handlePacket(packets.next()); + handlePacket(packets.next(), sender, senderPort); } } else { // Convert to message @@ -126,18 +127,47 @@ void OscServer::handlePacket(const OSCPP::Server::Packet& packet) if (msg == "/config") { const char* key = args.string(); const float value = args.float32(); - cout << "Config received - key (" << key << ") value (" << value << ")" - << endl; + cout << "OSC: Config received - key (" << key << ") value (" << value + << ")" << endl; if (strcmp("queueBuffer", key) == 0) { emit signalQueueBufferChanged(static_cast(value)); } + } else if (msg == "/get") { + const char* key = args.string(); + cout << "OSC: Get request received - key (" << key << ")" << endl; + if (strcmp("latency", key) == 0) { + emit signalLatencyRequested(sender, senderPort); + } } else { // Simply print unknown messages - cout << "Unknown message:" << msg.address() << endl; + cerr << "OSC: Unknown message:" << msg.address() << endl; } } - } catch (std::exception& e) { - cout << "Exception:" << e.what() << endl; + } catch (exception& e) { + cerr << "OSC: Exception:" << e.what() << endl; } } #endif // NO_OSCPP + +void OscServer::sendLatencyResponse(const QHostAddress& sender, quint16 senderPort, + QVector& clientNames, + QVector& latencies) +{ +#ifndef NO_OSCPP + QByteArray datagram; + datagram.resize(64 * 1024); + + OSCPP::Client::Packet packet(datagram.data(), 64 * 1024); + packet.openBundle(QDateTime::currentSecsSinceEpoch()); + packet.openMessage("/response/latency", clientNames.size() * 2); + for (int i = 0; i < clientNames.size(); i++) { + packet.string(clientNames[i].toStdString().c_str()); + packet.float32(latencies[i]); + } + packet.closeMessage(); + packet.closeBundle(); + + datagram.resize(packet.size()); + mOscServerSocket->writeDatagram(datagram, sender, senderPort); +#endif // NO_OSCPP +} diff --git a/src/OscServer.h b/src/OscServer.h index 5b2b55c..a860b38 100644 --- a/src/OscServer.h +++ b/src/OscServer.h @@ -37,8 +37,11 @@ #ifndef __OSCSERVER_H__ #define __OSCSERVER_H__ +#include #include +#include #include +#include #include #ifndef NO_OSCPP @@ -57,6 +60,8 @@ class OscServer : public QObject virtual ~OscServer(); void start(); void stop(); + void sendLatencyResponse(const QHostAddress& sender, quint16 senderPort, + QVector& clientNames, QVector& latencies); static size_t makeConfigPacket(void* buffer, size_t size, const char* key, float value) @@ -83,6 +88,7 @@ class OscServer : public QObject } signals: void signalQueueBufferChanged(int queueBufferSize); + void signalLatencyRequested(QHostAddress sender, quint16 senderPort); private slots: void readPendingDatagrams(); @@ -90,7 +96,8 @@ class OscServer : public QObject private: void closeSocket(); #ifndef NO_OSCPP - void handlePacket(const OSCPP::Server::Packet& packet); + void handlePacket(const OSCPP::Server::Packet& packet, const QHostAddress& sender, + quint16 senderPort); #endif // NO_OSCPP QSharedPointer mOscServerSocket; diff --git a/src/Regulator.cpp b/src/Regulator.cpp index faffb2c..39fe2f4 100644 --- a/src/Regulator.cpp +++ b/src/Regulator.cpp @@ -104,7 +104,7 @@ constexpr double AutoInitValFactor = // tweak constexpr int WindowDivisor = 8; // for faster auto tracking constexpr double AutoHeadroomGlitchTolerance = - 0.01; // Acceptable rate of glitches before auto headroom is increased (1.0%) + 0.006; // Acceptable rate of glitches before auto headroom is increased (0.6%) constexpr double AutoHistoryWindow = 60; // rolling window of time (in seconds) over which auto tolerance roughly adjusts constexpr double AutoSmoothingFactor = @@ -479,22 +479,23 @@ void Regulator::updateTolerance(int glitches, int skipped) // update headroom if (mAutoHeadroom < 0) { // variable headroom: automatically increase to minimize glitch counts - // only increase headroom if doing so would have reduced the number of - // glitches that occured over the past second by 1% or more. - // prevent headroom from growing beyond rolling average of max. - const int maxHeadroom = pushStat->longTermMax + 1; int glitchesAllowed; if (mMsecTolerance >= (mPeerFPPdurMsec * 2)) { - // calculate glitches allowed if tolerance if above or equal to duration of - // two packets - glitchesAllowed = - static_cast(AutoHeadroomGlitchTolerance * mSampleRate / mPeerFPP); + // calculate glitches allowed if tolerance is above or equal to + // the duration of two packets + glitchesAllowed = std::ceil( + static_cast(AutoHeadroomGlitchTolerance * mSampleRate) / mPeerFPP); } else { // zero glitches allowed if tolerance is below duration of two packets glitchesAllowed = 0; // also don't require two intervals in a row (override) mSkipAutoHeadroom = false; } + // sanity check: prevent headroom from growing beyond the greater of + // 3x rolling average of max, or 100ms + const int maxHeadroom = std::max(pushStat->longTermMax * 3, 100.0); + // only increase headroom if glitch tolerance was exceeded and doing so + // would have reduced the number of glitches that occured over the past second. if (skipped > 0 && glitches > glitchesAllowed && mCurrentHeadroom + 1 <= maxHeadroom) { if (mSkipAutoHeadroom) { diff --git a/src/RtAudioInterface.cpp b/src/RtAudioInterface.cpp index 8ff1599..893c6b5 100644 --- a/src/RtAudioInterface.cpp +++ b/src/RtAudioInterface.cpp @@ -491,6 +491,15 @@ void RtAudioInterface::setup(bool verbose) setDevicesWarningMsg(AudioInterface::DEVICE_WARN_BUFFER_LATENCY); } + if (mDuplexMode) { + // duplex mode returns sum of input and output latencies + mAudioInputLatency = static_cast(mRtAudioInput->getStreamLatency()) / 2; + mAudioOutputLatency = mAudioInputLatency; + } else { + mAudioInputLatency = mRtAudioInput->getStreamLatency(); + mAudioOutputLatency = mRtAudioOutput->getStreamLatency(); + } + // Setup parent class // This MUST be after buffer size is finalized, so that plugins // are initialized with the correct settings @@ -795,9 +804,6 @@ int RtAudioInterface::stopProcess() return (-1); } - AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE); - AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE); - return 0; } diff --git a/src/UdpHubListener.cpp b/src/UdpHubListener.cpp index 1d602dc..e6fd733 100644 --- a/src/UdpHubListener.cpp +++ b/src/UdpHubListener.cpp @@ -379,6 +379,28 @@ void UdpHubListener::queueBufferChanged(int queueBufferSize) } } +void UdpHubListener::handleLatencyRequest(const QHostAddress& sender, quint16 senderPort) +{ + QVector clientNames; + QVector latencies; + getClientLatencies(clientNames, latencies); + if (mOscServer != nullptr) { + mOscServer->sendLatencyResponse(sender, senderPort, clientNames, latencies); + } +} + +void UdpHubListener::getClientLatencies(QVector& clientNames, + QVector& latencies) +{ + QMutexLocker lock(&mMutex); + for (int i = 0; i < gMaxThreads; i++) { + if (mJTWorkers->at(i) != nullptr) { + clientNames.append(mJTWorkers->at(i)->getAssignedClientName()); + latencies.append(mJTWorkers->at(i)->getLatency()); + } + } +} + //******************************************************************************* // Returns 0 on error int UdpHubListener::readClientUdpPort(QSslSocket* clientConnection, QString& clientName) diff --git a/src/UdpHubListener.h b/src/UdpHubListener.h index 1570180..84e53a7 100644 --- a/src/UdpHubListener.h +++ b/src/UdpHubListener.h @@ -40,9 +40,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -86,6 +88,7 @@ class UdpHubListener : public QObject #endif int releaseThread(int id); void releaseDuplicateThreads(JackTripWorker* worker, uint16_t actual_peer_port); + void getClientLatencies(QVector& clientNames, QVector& latencies); void setConnectDefaultAudioPorts(bool connectDefaultAudioPorts) { @@ -111,6 +114,7 @@ class UdpHubListener : public QObject void receivedNewConnection(); void stopCheck(); void queueBufferChanged(int queueBufferSize); + void handleLatencyRequest(const QHostAddress& sender, quint16 senderPort); signals: void signalStarted(); @@ -139,6 +143,8 @@ class UdpHubListener : public QObject QObject::connect(mOscServer, &OscServer::signalQueueBufferChanged, this, &UdpHubListener::queueBufferChanged, Qt::QueuedConnection); + QObject::connect(mOscServer, &OscServer::signalLatencyRequested, this, + &UdpHubListener::handleLatencyRequest, Qt::QueuedConnection); }; /** diff --git a/src/gui/qjacktrip.cpp b/src/gui/qjacktrip.cpp index aec0546..78790fd 100644 --- a/src/gui/qjacktrip.cpp +++ b/src/gui/qjacktrip.cpp @@ -45,6 +45,12 @@ #include "RtAudio.h" #endif +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) +#define QCHECKBOX_STATE_CHANGED QCheckBox::stateChanged +#else +#define QCHECKBOX_STATE_CHANGED QCheckBox::checkStateChanged +#endif + #include "../Compressor.h" #include "../CompressorPresets.h" #include "../Limiter.h" @@ -119,14 +125,14 @@ QJackTrip::QJackTrip(UserInterface& interface, QWidget* parent) m_ui->patchServerCheckBox->setEnabled(false); } }); - connect(m_ui->authCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->authCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->usernameLabel->setEnabled(m_ui->authCheckBox->isChecked()); m_ui->usernameEdit->setEnabled(m_ui->authCheckBox->isChecked()); m_ui->passwordLabel->setEnabled(m_ui->authCheckBox->isChecked()); m_ui->passwordEdit->setEnabled(m_ui->authCheckBox->isChecked()); credentialsChanged(); }); - connect(m_ui->requireAuthCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->requireAuthCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->certLabel->setEnabled(m_ui->requireAuthCheckBox->isChecked()); m_ui->certEdit->setEnabled(m_ui->requireAuthCheckBox->isChecked()); m_ui->certBrowse->setEnabled(m_ui->requireAuthCheckBox->isChecked()); @@ -138,21 +144,21 @@ QJackTrip::QJackTrip(UserInterface& interface, QWidget* parent) m_ui->credsBrowse->setEnabled(m_ui->requireAuthCheckBox->isChecked()); authFilesChanged(); }); - connect(m_ui->ioStatsCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->ioStatsCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->ioStatsLabel->setEnabled(m_ui->ioStatsCheckBox->isChecked()); m_ui->ioStatsSpinBox->setEnabled(m_ui->ioStatsCheckBox->isChecked()); if (!m_ui->ioStatsCheckBox->isChecked()) { m_statsDialog->hide(); } }); - connect(m_ui->verboseCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->verboseCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { gVerboseFlag = m_ui->verboseCheckBox->isChecked(); if (!gVerboseFlag) { m_debugDialog->hide(); m_debugDialog->clearOutput(); } }); - connect(m_ui->jitterCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->jitterCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->broadcastCheckBox->setEnabled(m_ui->jitterCheckBox->isChecked()); m_ui->broadcastQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked() && m_ui->broadcastCheckBox->isChecked()); @@ -177,13 +183,13 @@ QJackTrip::QJackTrip(UserInterface& interface, QWidget* parent) m_autoQueueIndicator.setText(QStringLiteral("Auto queue: disabled")); } }); - connect(m_ui->broadcastCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->broadcastCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->broadcastQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked() && m_ui->broadcastCheckBox->isChecked()); m_ui->broadcastQueueSpinBox->setEnabled(m_ui->jitterCheckBox->isChecked() && m_ui->broadcastCheckBox->isChecked()); }); - connect(m_ui->autoQueueCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->autoQueueCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->autoQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked() && m_ui->autoQueueCheckBox->isChecked()); m_ui->autoQueueSpinBox->setEnabled(m_ui->jitterCheckBox->isChecked() @@ -199,34 +205,34 @@ QJackTrip::QJackTrip(UserInterface& interface, QWidget* parent) } }); - connect(m_ui->inFreeverbCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->inFreeverbCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->inFreeverbLabel->setEnabled(m_ui->inFreeverbCheckBox->isChecked()); m_ui->inFreeverbWetnessSlider->setEnabled(m_ui->inFreeverbCheckBox->isChecked()); }); - connect(m_ui->inZitarevCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->inZitarevCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->inZitarevLabel->setEnabled(m_ui->inZitarevCheckBox->isChecked()); m_ui->inZitarevWetnessSlider->setEnabled(m_ui->inZitarevCheckBox->isChecked()); }); - connect(m_ui->outFreeverbCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->outFreeverbCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->outFreeverbLabel->setEnabled(m_ui->outFreeverbCheckBox->isChecked()); m_ui->outFreeverbWetnessSlider->setEnabled( m_ui->outFreeverbCheckBox->isChecked()); }); - connect(m_ui->outZitarevCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->outZitarevCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->outZitarevLabel->setEnabled(m_ui->outZitarevCheckBox->isChecked()); m_ui->outZitarevWetnessSlider->setEnabled(m_ui->outZitarevCheckBox->isChecked()); }); - connect(m_ui->outLimiterCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->outLimiterCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->outLimiterLabel->setEnabled(m_ui->outLimiterCheckBox->isChecked()); m_ui->outClientsSpinBox->setEnabled(m_ui->outLimiterCheckBox->isChecked()); }); - connect(m_ui->connectScriptCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->connectScriptCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->connectScriptEdit->setEnabled(m_ui->connectScriptCheckBox->isChecked()); m_ui->connectScriptBrowse->setEnabled(m_ui->connectScriptCheckBox->isChecked()); }); - connect(m_ui->disconnectScriptCheckBox, &QCheckBox::stateChanged, this, [=]() { + connect(m_ui->disconnectScriptCheckBox, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_ui->disconnectScriptEdit->setEnabled( m_ui->disconnectScriptCheckBox->isChecked()); m_ui->disconnectScriptBrowse->setEnabled( @@ -426,7 +432,7 @@ void QJackTrip::showEvent(QShowEvent* event) "will automatically be re-enabled.)"); msgBox.setWindowTitle(QStringLiteral("JACK Not Available")); msgBox.setCheckBox(dontBugMe); - QObject::connect(dontBugMe, &QCheckBox::stateChanged, this, [=]() { + QObject::connect(dontBugMe, &QCHECKBOX_STATE_CHANGED, this, [=]() { m_hideWarning = dontBugMe->isChecked(); }); msgBox.exec(); diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index dbeef2a..aa9f9e9 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -40,7 +40,7 @@ #include "jacktrip_types.h" -constexpr const char* const gVersion = "2.5.1"; ///< JackTrip version +constexpr const char* const gVersion = "2.6.0"; ///< JackTrip version //******************************************************************************* /// \name Default Values diff --git a/src/vs/AudioSettings.qml b/src/vs/AudioSettings.qml index a02a8de..447d97a 100644 --- a/src/vs/AudioSettings.qml +++ b/src/vs/AudioSettings.qml @@ -452,7 +452,6 @@ Rectangle { delegate: ItemDelegate { required property var modelData required property int index - width: parent.width contentItem: Text { text: modelData.label } diff --git a/src/vs/ChangeDevices.qml b/src/vs/ChangeDevices.qml index dcff434..349e1f8 100644 --- a/src/vs/ChangeDevices.qml +++ b/src/vs/ChangeDevices.qml @@ -41,11 +41,17 @@ Rectangle { property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525" + property bool autoQueueBuffer: virtualstudio.queueBuffer == 0 + function getQueueBufferString () { - if (virtualstudio.queueBuffer == 0) { + let queueBuffer = virtualstudio.queueBuffer; + if (useStudioQueueBuffer.checkState == Qt.Checked) { + queueBuffer = virtualstudio.currentStudio.queueBuffer; + } + if (queueBuffer == 0) { return "auto"; } - return virtualstudio.queueBuffer + " ms"; + return queueBuffer + " ms"; } MouseArea { @@ -132,7 +138,7 @@ Rectangle { CheckBox { id: useStudioQueueBuffer checked: virtualstudio.useStudioQueueBuffer - text: qsTr("Use Studio settings") + text: qsTr("Use Studio settings (recommended)") anchors.top: latencyDivider.bottom anchors.topMargin: 16 * virtualstudio.uiScale x: 168 * virtualstudio.uiScale; @@ -167,32 +173,73 @@ Rectangle { } Text { - id: currentLatency + id: queueBufferText anchors.top: latencyDivider.bottom anchors.topMargin: 16 * virtualstudio.uiScale anchors.right: parent.right anchors.rightMargin: 24 * virtualstudio.uiScale - text: "Buffer Latency: " + Math.round(virtualstudio.networkStats.recvLatency) + " ms" + text: "Audio Quality: " + getQueueBufferString() + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + } + + Text { + id: currentLatency + anchors.top: queueBufferText.bottom + anchors.topMargin: 6 * virtualstudio.uiScale + anchors.right: parent.right + anchors.rightMargin: 24 * virtualstudio.uiScale + text: "Ingress Jitter Latency: " + Math.round(virtualstudio.networkStats.clientBufferLatency) + " ms" font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } color: textColour } + Button { + id: queueBufferAutoButton + width: 60 * virtualstudio.uiScale + height: 30 * virtualstudio.uiScale + anchors.top: useStudioQueueBuffer.bottom + anchors.topMargin: 16 * virtualstudio.uiScale + anchors.left: useStudioQueueBuffer.left + background: Rectangle { + radius: 6 * virtualstudio.uiScale + color: queueBufferAutoButton.down ? browserButtonPressedColour : (queueBufferAutoButton.hovered ? browserButtonHoverColour : (autoQueueBuffer ? "#FF0000" : browserButtonColour)) + } + onClicked: { + if (autoQueueBuffer) { + virtualstudio.queueBuffer = 5; + } else { + virtualstudio.queueBuffer = 0; + } + autoQueueBuffer = !autoQueueBuffer; + } + Text { + text: "Auto" + font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale} + anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } + color: textColour + } + + visible: useStudioQueueBuffer.checkState != Qt.Checked + } + Slider { id: queueBufferSlider value: virtualstudio.queueBuffer onMoved: { virtualstudio.queueBuffer = value; } - from: 0 + from: 1 to: 250 stepSize: 1 padding: 0 - visible: useStudioQueueBuffer.checkState != Qt.Checked + visible: !autoQueueBuffer && useStudioQueueBuffer.checkState != Qt.Checked anchors.top: useStudioQueueBuffer.bottom anchors.topMargin: 16 * virtualstudio.uiScale - x: queueBufferText.x + queueBufferText.width - width: parent.width - x - (16 * virtualstudio.uiScale) - queueBufferText.width; + anchors.left: queueBufferAutoButton.right + anchors.leftMargin: 8 * virtualstudio.uiScale + width: parent.width - x - (16 * virtualstudio.uiScale); background: Rectangle { x: queueBufferSlider.leftPadding @@ -224,14 +271,27 @@ Rectangle { } Text { - id: queueBufferText - width: (64 * virtualstudio.uiScale) - anchors.left: useStudioQueueBuffer.left - anchors.verticalCenter: queueBufferSlider.verticalCenter - text: getQueueBufferString() - font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale } + id: lowerLatencyText + anchors.top: queueBufferSlider.bottom + anchors.topMargin: 8 * virtualstudio.uiScale + anchors.left: queueBufferSlider.left + anchors.leftMargin: 8 * virtualstudio.uiScale + text: "Lower Latency" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } color: textColour - visible: useStudioQueueBuffer.checkState != Qt.Checked + visible: !autoQueueBuffer && useStudioQueueBuffer.checkState != Qt.Checked + } + + Text { + id: higherQualityText + anchors.top: queueBufferSlider.bottom + anchors.topMargin: 8 * virtualstudio.uiScale + anchors.right: queueBufferSlider.right + anchors.rightMargin: 8 * virtualstudio.uiScale + text: "Higher Quality" + font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } + color: textColour + visible: !autoQueueBuffer && useStudioQueueBuffer.checkState != Qt.Checked } } diff --git a/src/vs/Connected.qml b/src/vs/Connected.qml index b95af9f..85e73a1 100644 --- a/src/vs/Connected.qml +++ b/src/vs/Connected.qml @@ -60,6 +60,10 @@ Item { property bool isUsingRtAudio: audio.audioBackend == "RtAudio" + function hasStudioId () { + return typeof virtualstudio.currentStudio.id === 'string' && virtualstudio.currentStudio.id !== null && virtualstudio.currentStudio.id.length > 0 + } + Loader { id: studioWebLoader anchors.top: parent.top @@ -67,10 +71,7 @@ Item { anchors.left: parent.left anchors.bottom: deviceControlsGroup.top - property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" - property string studioId: virtualstudio.currentStudio.id - - source: accessToken && studioId ? "Web.qml" : "WebNull.qml" + source: auth.isAuthenticated && hasStudioId() ? "Web.qml" : "WebNull.qml" } DeviceControlsGroup { diff --git a/src/vs/CreateStudio.qml b/src/vs/CreateStudio.qml index 7458dd6..c796dae 100644 --- a/src/vs/CreateStudio.qml +++ b/src/vs/CreateStudio.qml @@ -20,8 +20,7 @@ Item { anchors.right: parent.right anchors.left: parent.left anchors.bottom: footer.top - property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" - sourceComponent: virtualstudio.windowState === "create_studio" && accessToken ? createStudioWeb : createStudioNull + sourceComponent: virtualstudio.windowState === "create_studio" && auth.isAuthenticated ? createStudioWeb : createStudioNull } Component { @@ -41,7 +40,7 @@ Item { settings.javascriptCanPaste: true settings.screenCaptureEnabled: true profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}` - url: `https://${virtualstudio.apiHost === "test.jacktrip.com" ? "next-test.jacktrip.com" : "www.jacktrip.com"}/app/studios/create?accessToken=${accessToken}&userId=${auth.userId}` + url: `https://${virtualstudio.apiHost === "test.jacktrip.com" ? "next-test.jacktrip.com" : "www.jacktrip.com"}/app/studios/create` onContextMenuRequested: function(request) { // this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop diff --git a/src/vs/DeviceWarningModal.qml b/src/vs/DeviceWarningModal.qml index f56dfa7..66fa616 100644 --- a/src/vs/DeviceWarningModal.qml +++ b/src/vs/DeviceWarningModal.qml @@ -132,7 +132,6 @@ Item { onClicked: () => { deviceWarningPopup.close(); audio.stopAudio(true); - virtualstudio.studioToJoin = virtualstudio.currentStudio.id; virtualstudio.windowState = "connected"; virtualstudio.saveSettings(); virtualstudio.joinStudio(); diff --git a/src/vs/FeedbackSurvey.qml b/src/vs/FeedbackSurvey.qml index bab87a3..678b503 100644 --- a/src/vs/FeedbackSurvey.qml +++ b/src/vs/FeedbackSurvey.qml @@ -371,7 +371,7 @@ Item { anchors.topMargin: 16 * virtualstudio.uiScale anchors.horizontalCenter: parent.horizontalCenter width: parent.width - text: "Your feedback has been recorded." + text: "Your feedback has been sent." font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale } horizontalAlignment: Text.AlignHCenter color: textColour diff --git a/src/vs/Footer.qml b/src/vs/Footer.qml index 18f8292..5b85ead 100644 --- a/src/vs/Footer.qml +++ b/src/vs/Footer.qml @@ -31,6 +31,7 @@ Rectangle { let minRtt = virtualstudio.networkStats.minRtt; let maxRtt = virtualstudio.networkStats.maxRtt; let avgRtt = virtualstudio.networkStats.avgRtt; + let clientBufferLatency = virtualstudio.networkStats.clientBufferLatency; let texts = ["Unstable", "Please plug into Ethernet & turn off WIFI.", meterRed]; if (virtualstudio.networkOutage) { @@ -42,16 +43,16 @@ Rectangle { return texts; } - texts[1] = "" + minRtt + " ms - " + maxRtt + " ms, avg " + avgRtt + " ms"; + texts[1] = "" + minRtt + " - " + maxRtt + " ms ping, " + clientBufferLatency + " ms jitter"; let quality = "Poor"; let color = meterRed; - if (avgRtt < 10 && maxRtt < 15) { + if (avgRtt < 10 && maxRtt < 15 && clientBufferLatency < 6) { quality = "Excellent"; color = meterGreen; - } else if (avgRtt < 20 && maxRtt < 30) { + } else if (avgRtt < 20 && maxRtt < 30 && clientBufferLatency < 9) { quality = "Good"; color = meterYellow; - } else if (avgRtt < 30 && maxRtt < 40) { + } else if (avgRtt < 30 && maxRtt < 40 && clientBufferLatency < 12) { quality = "Fair"; color = statsOrange; } diff --git a/src/vs/Setup.qml b/src/vs/Setup.qml index a28a8dd..e2fe2cf 100644 --- a/src/vs/Setup.qml +++ b/src/vs/Setup.qml @@ -140,7 +140,6 @@ Item { deviceWarningModal.open(); } else { audio.stopAudio(true); - virtualstudio.studioToJoin = virtualstudio.currentStudio.id; virtualstudio.windowState = "connected"; virtualstudio.saveSettings(); virtualstudio.joinStudio(); diff --git a/src/vs/WebEngine.qml b/src/vs/WebEngine.qml index 2f15764..192d184 100644 --- a/src/vs/WebEngine.qml +++ b/src/vs/WebEngine.qml @@ -52,7 +52,6 @@ Item { anchors.fill: parent color: backgroundColour - property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" property string studioId: virtualstudio.currentStudio.id WebEngineView { @@ -62,7 +61,7 @@ Item { settings.javascriptCanPaste: true settings.screenCaptureEnabled: true profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}` - url: `https://${virtualstudio.apiHost}/studios/${studioId}/live` + url: `https://${virtualstudio.apiHost}/studios/${web.studioId}/live` // useful for debugging // onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) { diff --git a/src/vs/WebView.qml b/src/vs/WebView.qml index 871a1d9..8590542 100644 --- a/src/vs/WebView.qml +++ b/src/vs/WebView.qml @@ -10,14 +10,13 @@ Item { id: web anchors.fill: parent - property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : "" property string studioId: virtualstudio.currentStudio.id WebView { id: webEngineView anchors.fill: parent httpUserAgent: `JackTrip/${virtualstudio.versionString}` - url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}` + url: `https://${virtualstudio.apiHost}/studios/${web.studioId}/live` } } } diff --git a/src/vs/virtualstudio.cpp b/src/vs/virtualstudio.cpp index bef8af4..7ba6368 100644 --- a/src/vs/virtualstudio.cpp +++ b/src/vs/virtualstudio.cpp @@ -592,7 +592,7 @@ void VirtualStudio::setWindowState(QString state) m_windowState = state; // refresh studio list if navigating to browse window // only if user id is empty (edge case for when logging in) - if (m_windowState == "browse" && !m_userId.isEmpty()) { + if (m_windowState == "browse" && m_auth->isAuthenticated()) { // schedule studio refresh instead of doing it now // just to reduce risk of running into a deadlock emit scheduleStudioRefresh(-1, false); @@ -655,26 +655,22 @@ void VirtualStudio::collectFeedbackSurvey(QString serverId, int rating, QString #endif feedback.insert(QStringLiteral("osVersion"), QSysInfo::prettyProductName()); - QString sysInfo = QString("[platform=%1").arg(QSysInfo::prettyProductName()); #ifdef RT_AUDIO QString inputDevice = QString::fromStdString(m_audioConfigPtr->getInputDevice().toStdString()); if (!inputDevice.isEmpty()) { - sysInfo.append(QString(",input=%1").arg(inputDevice)); + feedback.insert(QStringLiteral("inputDevice"), inputDevice); } QString outputDevice = QString::fromStdString(m_audioConfigPtr->getOutputDevice().toStdString()); if (!outputDevice.isEmpty()) { - sysInfo.append(QString(",output=%1").arg(outputDevice)); + feedback.insert(QStringLiteral("outputDevice"), outputDevice); } #endif - sysInfo.append("]"); feedback.insert(QStringLiteral("rating"), rating); - if (message.isEmpty()) { - feedback.insert(QStringLiteral("message"), sysInfo); - } else { - feedback.insert(QStringLiteral("message"), message + " " + sysInfo); + if (!message.isEmpty()) { + feedback.insert(QStringLiteral("message"), message); } QString deviceIssues = ""; @@ -687,11 +683,11 @@ void VirtualStudio::collectFeedbackSurvey(QString serverId, int rating, QString } if (!deviceIssues.isEmpty()) { feedback.insert(QStringLiteral("deviceIssues"), deviceIssues); - message.append(" (deviceIssues=" + deviceIssues + ")"); } QJsonDocument data = QJsonDocument(feedback); m_api->submitServerFeedback(serverId, data.toJson()); + std::cout << "Sent feedback: " << data.toJson().toStdString() << std::endl; return; } @@ -857,17 +853,13 @@ void VirtualStudio::joinStudio() return; } - // pop studioToJoin - const QString targetId = m_studioToJoin; - setStudioToJoin(""); - // stop audio if already running (settings or setup windows) m_audioConfigPtr->stopAudio(true); // find and populate data for current studio VsServerInfoPointer sPtr; for (const VsServerInfoPointer& s : m_servers) { - if (s->id() == targetId) { + if (s->id() == m_studioToJoin) { sPtr = s; break; } @@ -875,21 +867,23 @@ void VirtualStudio::joinStudio() locker.unlock(); if (sPtr.isNull()) { - m_failedMessage = "Unable to find studio " + targetId; + m_failedMessage = "Unable to find studio " + m_studioToJoin; + setStudioToJoin(""); emit failedMessageChanged(); emit failed(); return; } - m_currentStudio = *sPtr; - emit currentStudioChanged(); - if (m_windowState == "setup") { - m_audioConfigPtr->setSampleRate(m_currentStudio.sampleRate()); + m_audioConfigPtr->setSampleRate(sPtr->sampleRate()); m_audioConfigPtr->startAudio(); return; } + setStudioToJoin(""); + m_currentStudio = *sPtr; + emit currentStudioChanged(); + // m_windowState == "connected" connectToStudio(); } @@ -1168,6 +1162,9 @@ void VirtualStudio::triggerReconnect(bool refresh) return; } + std::cout << "Reconnecting audio to " << m_currentStudio.host().toStdString() << ":" + << m_currentStudio.port() << std::endl; + // this needs to be synchronous to avoid both trying // to use the audio interfaces at the same time // note that connectionFinished() checks m_reconnectState @@ -1501,11 +1498,18 @@ void VirtualStudio::receivedConnectionFromPeer() void VirtualStudio::handleWebsocketMessage(const QString& msg) { - QJsonObject serverState = QJsonDocument::fromJson(msg.toUtf8()).object(); - QString serverStatus = serverState[QStringLiteral("status")].toString(); - bool serverEnabled = serverState[QStringLiteral("enabled")].toBool(); - QString serverCloudId = serverState[QStringLiteral("cloudId")].toString(); - int queueBuffer = serverState[QStringLiteral("queueBuffer")].toInt(); + if (m_currentStudio.id() == "") { + return; + } + + QJsonObject serverState = QJsonDocument::fromJson(msg.toUtf8()).object(); + const QString& serverHost = serverState[QStringLiteral("serverHost")].toString(); + const QString& serverStatus = serverState[QStringLiteral("status")].toString(); + const QString& serverCloudId = serverState[QStringLiteral("cloudId")].toString(); + const QString& sessionId = serverState[QStringLiteral("sessionId")].toString(); + const bool serverEnabled = serverState[QStringLiteral("enabled")].toBool(); + const int serverPort = serverState[QStringLiteral("serverPort")].toInt(); + const int queueBuffer = serverState[QStringLiteral("queueBuffer")].toInt(); // server notifications are also transmitted along this websocket, so ignore data if // it contains "message" @@ -1513,26 +1517,55 @@ void VirtualStudio::handleWebsocketMessage(const QString& msg) if (!message.isEmpty()) { return; } - if (m_currentStudio.id() == "") { - return; + + bool currentStudioUpdated = false; + bool serverHostOrPortUpdated = false; + if (serverHost != m_currentStudio.host()) { + m_currentStudio.setHost(serverHost); + currentStudioUpdated = true; + serverHostOrPortUpdated = true; } - m_currentStudio.setStatus(serverStatus); - m_currentStudio.setEnabled(serverEnabled); - m_currentStudio.setCloudId(serverCloudId); - m_currentStudio.setQueueBuffer(queueBuffer); - if (!m_jackTripRunning) { - if (serverStatus == QLatin1String("Ready") && m_onConnectedScreen) { - m_currentStudio.setHost(serverState[QStringLiteral("serverHost")].toString()); - m_currentStudio.setPort(serverState[QStringLiteral("serverPort")].toInt()); - m_currentStudio.setSessionId( - serverState[QStringLiteral("sessionId")].toString()); - completeConnection(); + if (serverStatus != m_currentStudio.status()) { + m_currentStudio.setStatus(serverStatus); + currentStudioUpdated = true; + } + if (serverCloudId != m_currentStudio.cloudId()) { + m_currentStudio.setCloudId(serverCloudId); + currentStudioUpdated = true; + } + if (sessionId != m_currentStudio.sessionId()) { + m_currentStudio.setSessionId(sessionId); + currentStudioUpdated = true; + } + if (serverEnabled != m_currentStudio.enabled()) { + m_currentStudio.setEnabled(serverEnabled); + currentStudioUpdated = true; + } + if (serverPort != m_currentStudio.port()) { + m_currentStudio.setPort(serverPort); + currentStudioUpdated = true; + serverHostOrPortUpdated = true; + } + if (queueBuffer != m_currentStudio.queueBuffer()) { + m_currentStudio.setQueueBuffer(queueBuffer); + currentStudioUpdated = true; + if (m_useStudioQueueBuffer && !m_devicePtr.isNull()) { + m_devicePtr->setQueueBuffer(m_currentStudio.queueBuffer()); } - } else if (m_useStudioQueueBuffer && !m_devicePtr.isNull()) { - m_devicePtr->setQueueBuffer(m_currentStudio.queueBuffer()); } - emit currentStudioChanged(); + if (currentStudioUpdated) { + emit currentStudioChanged(); + } + + if (m_onConnectedScreen) { + if (!m_jackTripRunning && serverEnabled && serverStatus == QLatin1String("Ready") + && serverHost != "" && serverPort != 0) { + std::cout << "Connecting audio to " << serverHost.toStdString() << ":" + << serverPort << std::endl; + completeConnection(); + } + } } void VirtualStudio::restartStudioSocket() @@ -1586,7 +1619,7 @@ void VirtualStudio::resetState() void VirtualStudio::refreshStudios(int index, bool signalRefresh) { // user id is required for retrieval of subscriptions - if (m_userId.isEmpty()) { + if (!m_auth->isAuthenticated()) { std::cerr << "Studio refresh cancelled due to empty user id" << std::endl; return; } @@ -1873,6 +1906,7 @@ VirtualStudio::~VirtualStudio() QApplication* VirtualStudio::createApplication(int& argc, char* argv[]) { #if defined(Q_OS_WIN) +#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) // Fix for display scaling like 125% or 150% on Windows QGuiApplication::setHighDpiScaleFactorRoundingPolicy( Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); @@ -1882,8 +1916,12 @@ QApplication* VirtualStudio::createApplication(int& argc, char* argv[]) // QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); // QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); + // Direct3D11 is still broken as of Qt 6.8.1 // QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D11); QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); +#else // Qt 6.6.0 or later supports Direct3D 12, which works well + QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D12); +#endif #endif QQuickStyle::setStyle("Basic"); @@ -1891,7 +1929,13 @@ QApplication* VirtualStudio::createApplication(int& argc, char* argv[]) #if defined(Q_OS_MACOS) && (QT_VERSION > QT_VERSION_CHECK(6, 2, 6)) \ && (QT_VERSION < QT_VERSION_CHECK(6, 8, 0)) // work-around for screen sharing bugs in qtwebengine 6.2.7-6.7.x - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", "--disable-features=DesktopCaptureMacV2"); + QString chromiumFlags("--disable-features=DesktopCaptureMacV2"); + char* existingFlags = getenv("QTWEBENGINE_CHROMIUM_FLAGS"); + if (existingFlags != nullptr) { + chromiumFlags.append(" "); + chromiumFlags.append(existingFlags); + } + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags.toUtf8()); #endif // Initialize webengine diff --git a/src/vs/vsAudio.cpp b/src/vs/vsAudio.cpp index d244238..09e34e9 100644 --- a/src/vs/vsAudio.cpp +++ b/src/vs/vsAudio.cpp @@ -315,6 +315,16 @@ void VsAudio::setInputMixMode(const int mode) if (mode == m_inputMixMode) return; m_inputMixMode = mode; + if (m_inputMixMode == static_cast(AudioInterface::MONO)) { + if (m_numInputChannels > 1) { + setNumInputChannels(1); + } + } else if (m_inputMixMode == static_cast(AudioInterface::STEREO) + || m_inputMixMode == static_cast(AudioInterface::MIXTOMONO)) { + if (m_numInputChannels == 1) { + setNumInputChannels(2); + } + } emit inputMixModeChanged(mode); return; } @@ -863,6 +873,11 @@ AudioInterface* VsAudio::newAudioInterface(JackTrip* jackTripPtr) std::cout << " or: " << AudioBufferSizeInBytes << " bytes" << std::endl; std::cout << gPrintSeparator << std::endl; + std::cout << "The Audio Input Latency is : " << ifPtr->getAudioInputLatency() + << std::endl; + std::cout << "The Audio Output Latency is: " << ifPtr->getAudioOutputLatency() + << std::endl; + std::cout << gPrintSeparator << std::endl; std::cout << "The Number of Channels is: " << ifPtr->getNumInputChannels() << std::endl; std::cout << gPrintSeparator << std::endl; @@ -1302,17 +1317,13 @@ void VsAudioWorker::validateInputDevicesState() inputChannelsComboModel.push_back(element); } for (int i = 0; i < numDevicesChannelsAvailable; i++) { - if (i % 2 == 0) { - QJsonObject element = QJsonObject(); - element.insert( - QString::fromStdString("label"), - QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); - element.insert(QString::fromStdString("baseChannel"), - QVariant(i).toInt()); - element.insert(QString::fromStdString("numChannels"), - QVariant(2).toInt()); - inputChannelsComboModel.push_back(element); - } + QJsonObject element = QJsonObject(); + element.insert( + QString::fromStdString("label"), + QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); + element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt()); + element.insert(QString::fromStdString("numChannels"), QVariant(2).toInt()); + inputChannelsComboModel.push_back(element); } m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel); @@ -1324,26 +1335,31 @@ void VsAudioWorker::validateInputDevicesState() m_parentPtr->setBaseInputChannel(0); m_parentPtr->setNumInputChannels(2); } - if (getNumInputChannels() != 1) { - // Set the input mix mode to have two options: "Stereo" and "Mix to Mono" if - // we're using 2 channels - QJsonObject inputMixModeComboElement1 = QJsonObject(); - inputMixModeComboElement1.insert(QString::fromStdString("label"), - QString::fromStdString("Stereo")); - inputMixModeComboElement1.insert(QString::fromStdString("value"), - static_cast(AudioInterface::STEREO)); - QJsonObject inputMixModeComboElement2 = QJsonObject(); - inputMixModeComboElement2.insert(QString::fromStdString("label"), - QString::fromStdString("Mix to Mono")); - inputMixModeComboElement2.insert(QString::fromStdString("value"), - static_cast(AudioInterface::MIXTOMONO)); - QJsonArray inputMixModeComboModel; - inputMixModeComboModel.push_back(inputMixModeComboElement1); - inputMixModeComboModel.push_back(inputMixModeComboElement2); - m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel); - - // if m_inputMixMode is an invalid value, set it to "stereo" by default - // given that we are using 2 channels + + // include all options in the mix mode combo + QJsonObject inputMixModeComboElement0 = QJsonObject(); + inputMixModeComboElement0.insert(QString::fromStdString("label"), + QString::fromStdString("Mono")); + inputMixModeComboElement0.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MONO)); + QJsonObject inputMixModeComboElement1 = QJsonObject(); + inputMixModeComboElement1.insert(QString::fromStdString("label"), + QString::fromStdString("Stereo")); + inputMixModeComboElement1.insert(QString::fromStdString("value"), + static_cast(AudioInterface::STEREO)); + QJsonObject inputMixModeComboElement2 = QJsonObject(); + inputMixModeComboElement2.insert(QString::fromStdString("label"), + QString::fromStdString("Mix to Mono")); + inputMixModeComboElement2.insert(QString::fromStdString("value"), + static_cast(AudioInterface::MIXTOMONO)); + QJsonArray inputMixModeComboModel; + inputMixModeComboModel.push_back(inputMixModeComboElement0); + inputMixModeComboModel.push_back(inputMixModeComboElement1); + inputMixModeComboModel.push_back(inputMixModeComboElement2); + m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel); + + if (m_parentPtr->getNumInputChannels() == 2) { + // Set the input mix mode to "Stereo" if we're using 2 channels if (getInputMixMode() != static_cast(AudioInterface::STEREO) && getInputMixMode() != static_cast(AudioInterface::MIXTOMONO)) { m_parentPtr->setInputMixMode(static_cast(AudioInterface::STEREO)); @@ -1351,16 +1367,6 @@ void VsAudioWorker::validateInputDevicesState() } else { // Set the input mix mode to just have "Mono" as the option if we're using 1 // channel - QJsonObject inputMixModeComboElement = QJsonObject(); - inputMixModeComboElement.insert(QString::fromStdString("label"), - QString::fromStdString("Mono")); - inputMixModeComboElement.insert(QString::fromStdString("value"), - static_cast(AudioInterface::MONO)); - QJsonArray inputMixModeComboModel; - inputMixModeComboModel.push_back(inputMixModeComboElement); - m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel); - - // if m_inputMixMode is an invalid value, set it to AudioInterface::MONO if (getInputMixMode() != static_cast(AudioInterface::MONO)) { m_parentPtr->setInputMixMode(static_cast(AudioInterface::MONO)); } @@ -1418,17 +1424,13 @@ void VsAudioWorker::validateOutputDevicesState() // selected device QJsonArray outputChannelsComboModel; for (int i = 0; i < numDevicesChannelsAvailable; i++) { - if (i % 2 == 0) { - QJsonObject element = QJsonObject(); - element.insert( - QString::fromStdString("label"), - QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); - element.insert(QString::fromStdString("baseChannel"), - QVariant(i).toInt()); - element.insert(QString::fromStdString("numChannels"), - QVariant(2).toInt()); - outputChannelsComboModel.push_back(element); - } + QJsonObject element = QJsonObject(); + element.insert( + QString::fromStdString("label"), + QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString()); + element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt()); + element.insert(QString::fromStdString("numChannels"), QVariant(2).toInt()); + outputChannelsComboModel.push_back(element); } m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel); diff --git a/src/vs/vsDevice.cpp b/src/vs/vsDevice.cpp index 8e49fd6..39d7cc9 100644 --- a/src/vs/vsDevice.cpp +++ b/src/vs/vsDevice.cpp @@ -189,8 +189,17 @@ void VsDevice::sendHeartbeat() json.insert(QLatin1String("high_latency"), m_audioConfigPtr->getHighLatencyFlag()); json.insert(QLatin1String("network_outage"), m_networkOutage); - json.insert(QLatin1String("recv_latency"), - m_jackTrip.isNull() ? -1 : m_jackTrip->getLatency()); + json.insert(QLatin1String("audio_input_latency"), + m_jackTrip.isNull() + ? 0 + : (qint64)(m_jackTrip->getAudioInputLatency() * 10000)); + json.insert(QLatin1String("audio_output_latency"), + m_jackTrip.isNull() + ? 0 + : (qint64)(m_jackTrip->getAudioOutputLatency() * 10000)); + json.insert( + QLatin1String("client_buffer_latency"), + m_jackTrip.isNull() ? 0 : (qint64)(m_jackTrip->getLatency() * ns_per_ms)); // For the internal application UI, ms will suffice. No conversion needed QJsonObject pingStats = {}; @@ -203,8 +212,13 @@ void VsDevice::sendHeartbeat() ((int)(10 * stats.stdDevRtt)) / 10.0); pingStats.insert(QLatin1String("highLatency"), m_audioConfigPtr->getHighLatencyFlag()); - pingStats.insert(QLatin1String("recvLatency"), - m_jackTrip.isNull() ? -1 : m_jackTrip->getLatency()); + pingStats.insert(QLatin1String("audioInputLatency"), + m_jackTrip.isNull() ? 0 : m_jackTrip->getAudioInputLatency()); + pingStats.insert(QLatin1String("audioOutputLatency"), + m_jackTrip.isNull() ? 0 : m_jackTrip->getAudioOutputLatency()); + pingStats.insert( + QLatin1String("clientBufferLatency"), + m_jackTrip.isNull() ? 0 : ((int)(10 * m_jackTrip->getLatency())) / 10.0); emit updateNetworkStats(pingStats); } -- 2.30.2